package com.artifex.mupdf.viewer;

import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import androidx.annotation.Nullable;

import com.artifex.mupdf.fitz.Cookie;
import com.artifex.mupdf.fitz.DisplayList;
import com.artifex.mupdf.fitz.Document;
import com.artifex.mupdf.fitz.Link;
import com.artifex.mupdf.fitz.Matrix;
import com.artifex.mupdf.fitz.Outline;
import com.artifex.mupdf.fitz.Page;
import com.artifex.mupdf.fitz.Quad;
import com.artifex.mupdf.fitz.Rect;
import com.artifex.mupdf.fitz.RectI;
import com.artifex.mupdf.fitz.StructuredText;
import com.artifex.mupdf.fitz.android.AndroidDrawDevice;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.mlkit.nl.translate.Translator;
import com.google.mlkit.nl.translate.TranslatorOptions;
import com.google.mlkit.samples.nl.translate.AppExecutor;
import com.google.mlkit.samples.nl.translate.AssetCopyHelper;
import com.google.mlkit.samples.nl.translate.MainHandler;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import kotlin.Triple;

public class TTSDocument implements SharedPreferences.OnSharedPreferenceChangeListener {
    private static final String APP = "TTSDocument";

    public Document doc;
    private Outline[] outline;
    private int pageCount = -1;
    private int currentPageNum = -1;
    private Page page;
    private float pageWidth;
    private float pageHeight;
    private DisplayList displayList;

    /* Default to "A Format" pocket book size. */
    private int layoutW = 312;
    private int layoutH = 504;
    private int layoutEM = 10;
    SharedPreferences prefs;
    SentencePatternManager regexMgr;
    public Uri uri;

    public TTSDocument(Context context, Uri uri) throws IOException {
        this.uri = uri;
        doc = openDocument(context, uri);
        prefs = context.getSharedPreferences(base64(uri.toString()), Context.MODE_PRIVATE);
        prefs.registerOnSharedPreferenceChangeListener(this);
        regexMgr = new SentencePatternManager(context, base64(uri.toString()));
        AssetCopyHelper.copyTranslateModels(context);
        TranslateCache.makeSureTranslate(context, uri, s -> {
            MainHandler.obtain().post(() -> Toast.makeText(context, s, Toast.LENGTH_LONG).show());
        }, e -> {
            MainHandler.obtain().post(() -> Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show());
        });
    }

    Document openDocument(Context context, Uri uri) throws IOException {
        Document tmpDoc = null;
        long size = -1;

        try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                int idx;

                idx = cursor.getColumnIndex(OpenableColumns.SIZE);
                if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_INTEGER)
                    size = cursor.getLong(idx);

                if (size == 0)
                    size = -1;
            }
        } catch (Exception x) {
            Log.e(APP, "fail to parse uri", x);
            throw x;
        }
        String mimetype = context.getContentResolver().getType(uri);
        if (mimetype == null) {
            mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(uri.toString()));
        }

        ContentResolver cr = context.getContentResolver();

        Log.i(APP, "Opening document " + uri);
        try {
            tmpDoc = Document.openDocument(new ContentInputStream(cr, uri, size), mimetype);

        } catch (OutOfMemoryError | IOException e) {
            Log.e(APP, "Open document fail", e);
            throw e;
        }

        return tmpDoc;
    }

    public synchronized void layout(int oldPage, int w, int h, int em) {

        if (w != layoutW || h != layoutH || em != layoutEM) {
//            System.out.println("LAYOUT: " + w + "," + h);
            layoutW = w;
            layoutH = h;
            layoutEM = em;
//            long mark = doc.makeBookmark(doc.locationFromPageNumber(oldPage));
            doc.layout(layoutW, layoutH, layoutEM);
            currentPageNum = -1;
            pageCount = doc.countPages();
            outline = null;
            try {
                outline = doc.loadOutline();
            } catch (Exception ex) {
                /* ignore error */
            }
//            return doc.pageNumberFromLocation(doc.findBookmark(mark));
        }
//        return oldPage;
    }

    private synchronized void gotoPage(int pageNum) {
        if (pageNum > pageCount - 1)
            pageNum = pageCount - 1;
        else if (pageNum < 0)
            pageNum = 0;
        if (pageNum != currentPageNum) {
            if (page != null)
                page.destroy();
            page = null;
            if (displayList != null)
                displayList.destroy();
            displayList = null;
            page = null;
            pageWidth = 0;
            pageHeight = 0;
            currentPageNum = -1;


            page = doc.loadPage(pageNum);
            Rect b = page.getBounds();
            pageWidth = b.x1 - b.x0;
            pageHeight = b.y1 - b.y0;


            currentPageNum = pageNum;
        }
    }

    public synchronized PointF getPageSize(int pageNum) {
        gotoPage(pageNum);
        return new PointF(pageWidth, pageHeight);
    }

    private boolean destroyed = false;

    public boolean destroyed() {
        return destroyed;
    }

    public synchronized void destroy() {
        Log.d("TTSDocument", "destroy");
        destroyed = true;
        if (displayList != null)
            displayList.destroy();
        displayList = null;
        if (page != null)
            page.destroy();
        page = null;

        prefs.unregisterOnSharedPreferenceChangeListener(this);
        doc.destroy();
        TranslateCache.obtain().evictAll();
    }

    public synchronized void drawPage(Bitmap bm, int pageNum,
                                      int pageW, int pageH,
                                      int patchX, int patchY,
                                      int patchW, int patchH,
                                      Cookie cookie) {
        //加载文字
        gotoPage(pageNum);
        if (page == null) return;

        if (displayList == null) {
            try {
                displayList = page.toDisplayList(false);
            } catch (Exception ex) {
                displayList = null;
            }
        }

        if (displayList == null)
            return;

//        float zoom = (float) resolution / 72;
//        Matrix ctm = new Matrix(zoom, zoom);
        Matrix ctm = new Matrix(1, 1);
        RectI bBox = new RectI(page.getBounds().transform(ctm));
        float xScale = (float) pageW / (float) (bBox.x1 - bBox.x0);
        float yScale = (float) pageH / (float) (bBox.y1 - bBox.y0);
        ctm.scale(xScale, yScale);

        AndroidDrawDevice dev = new AndroidDrawDevice(bm, patchX, patchY);
        try {
            displayList.run(dev, ctm, cookie);
            dev.close();
        } finally {
            dev.destroy();
        }
    }


    public synchronized void updatePage(Bitmap bm, int pageNum,
                                        int pageW, int pageH,
                                        int patchX, int patchY,
                                        int patchW, int patchH,
                                        Cookie cookie) {
        drawPage(bm, pageNum, pageW, pageH, patchX, patchY, patchW, patchH, cookie);
    }

    public synchronized Link[] getPageLinks(int pageNum) {
        gotoPage(pageNum);
        return page != null ? page.getLinks() : null;
    }

    public synchronized int resolve(Link link) {
        return doc.pageNumberFromLocation(doc.resolveLink(link));
    }

    public synchronized Quad[][] searchPage(int pageNum, String text) {
        gotoPage(pageNum);
        return page.search(text);
    }

    public synchronized Quad[][] find(String text) {
        return page.search(text);
    }

    public synchronized boolean hasOutline() {
        if (outline == null) {
            try {
                outline = doc.loadOutline();
            } catch (Exception ex) {
                /* ignore error */
            }
        }
        return outline != null;
    }

    private void flattenOutlineNodes(ArrayList<OutlineActivity.Item> result, Outline[] list, String indent) {
        for (Outline node : list) {
            if (node.title != null) {
                int page = doc.pageNumberFromLocation(doc.resolveLink(node));
                result.add(new OutlineActivity.Item(indent + node.title, page));
            }
            if (node.down != null)
                flattenOutlineNodes(result, node.down, indent + "    ");
        }
    }

    public synchronized ArrayList<OutlineActivity.Item> getOutline() {
        ArrayList<OutlineActivity.Item> result = new ArrayList<OutlineActivity.Item>();
        flattenOutlineNodes(result, outline, "");
        return result;
    }


    public synchronized boolean needsPassword() {
        return doc.needsPassword();
    }

    public synchronized boolean authenticatePassword(String password) {
        boolean authenticated = doc.authenticatePassword(password);
        pageCount = doc.countPages();
        return authenticated;
    }

    public synchronized Task<List<Triple<SettingsItem, String, Quad[][]>>> translate(List<Triple<SettingsItem, String, Quad[][]>> entries) {
        final TaskCompletionSource<List<Triple<SettingsItem, String, Quad[][]>>> taskCompletionSource = new TaskCompletionSource<>();
        if (entries == null || entries.isEmpty()) {
            taskCompletionSource.setResult(null);
            return taskCompletionSource.getTask();
        }
        SettingsItem fromSetting = entries.get(0).component1();

        Translator translator;
        SettingsItem toSetting;
        String toLanguage = prefs.getString(SettingsItem.TYPE.TO.name(), null);
        if (toLanguage != null) {
            toSetting = new SettingsItem();
            toSetting.language = new Locale(toLanguage);
            toSetting.speed = prefs.getFloat(SettingsItem.TYPE.tSPEED.name(), 1.5f);
            if (!fromSetting.language.equals(toSetting.language)) {
                translator = TranslateCache.obtain().get(new TranslatorOptions.Builder()
                        .setSourceLanguage(fromSetting.language.getLanguage())
                        .setTargetLanguage(toSetting.language.getLanguage())
                        .build());
            } else {
                translator = null;
            }
        } else {
            translator = null;
            toSetting = null;
        }

        if (translator != null) {
            Log.d("TTSDocument", "doing translating");
            if (!fromSetting.language.equals(toSetting.language)) {
                AppExecutor.obtain().submit(() -> {
                    ArrayList<Triple<SettingsItem, String, Quad[][]>> translated = new ArrayList<>();
                    try {
                        for (Triple<SettingsItem, String, Quad[][]> item : entries) {
                            translated.add(new Triple<>(toSetting, Tasks.await(translator.translate(item.component2()), 1, TimeUnit.SECONDS), item.component3()));
                        }
                    } catch (ExecutionException | InterruptedException |
                             TimeoutException error) {
                        Log.e(APP, "Translate failed", error);
                    }
                    Log.d("TTSDocument", "done translating");
                    taskCompletionSource.setResult(translated);
                });
            }
        }
        return taskCompletionSource.getTask();
    }

    public synchronized Task<List<Triple<SettingsItem, String, Quad[][]>>> getSegments(int pageNumber) {

        SettingsItem fromSetting = new SettingsItem();
        fromSetting.language = new Locale(prefs.getString(SettingsItem.TYPE.FROM.name(), Locale.ENGLISH.getLanguage()));
        fromSetting.speed = prefs.getFloat(SettingsItem.TYPE.fSPEED.name(), 1.25f);


        final TaskCompletionSource<List<Triple<SettingsItem, String, Quad[][]>>> taskCompletionSource = new TaskCompletionSource<>();

        AppExecutor.obtain().submit(() -> {


            // Validate page number
            if (pageNumber < 0 || pageNumber >= doc.countPages()) {
                taskCompletionSource.setException(new IllegalArgumentException("Invalid page number: " + pageNumber));
                return;
            }

            // Load the specific page
            Page page = doc.loadPage(pageNumber);
            StructuredText structuredText = page.toStructuredText();

            // Extract text from the page
            String text = structuredText.asText();

            if (text == null || text.trim().isEmpty()) {
                structuredText.destroy();
                page.destroy();

                taskCompletionSource.setResult(Collections.emptyList());
                return;
            }

            ArrayList<Triple<SettingsItem, String, Quad[][]>> entries = new ArrayList<>();
            for (String tmp3 : regexMgr.split(text.trim())) {

                tmp3 = tmp3.trim().replaceAll("^[\\W_]+|[\\W_]+$", "");
                String tmp4 = tmp3.replaceAll("[\n\r]", " ");
                if (tmp4.isEmpty() || tmp4.length() == 1)// Filter out very short sentences
                    continue;
                Quad[][] q = page.search(tmp3);

                entries.add(new Triple<>(fromSetting, tmp4, q));
            }

            // Release resources
            structuredText.destroy();
            page.destroy();

            taskCompletionSource.setResult(entries);
        });
        return taskCompletionSource.getTask();
    }

    public static String base64(String text) {
        if (text == null) return null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            return Base64.getUrlEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
        }
        return null;
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
        if (Objects.equals(key, "patterns")) {
            regexMgr.loadPatterns();
        }
    }
}